﻿using System;
using System.Runtime.CompilerServices;

namespace Aeon.Emulator.Video.Modes
{
    /// <summary>
    /// Implements functionality for 4-plane video modes.
    /// </summary>
    internal abstract class Planar4 : VideoMode
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Planar4 class.
        /// </summary>
        /// <param name="width">Width of the display mode in pixels.</param>
        /// <param name="height">Height of the display mode in pixels.</param>
        /// <param name="bpp">Color depth of the display mode in bits.</param>
        /// <param name="fontHeight">Height of the video mode's font in pixels.</param>
        /// <param name="modeType">Value indicating whether the mode is graphical or text.</param>
        /// <param name="video">Current VideoHandler instance.</param>
        public Planar4(int width, int height, int bpp, int fontHeight, VideoModeType modeType, VideoHandler video)
            : base(width, height, bpp, true, fontHeight, modeType, video)
        {
            unsafe
            {
                byte* vram = (byte*)video.VideoRam.ToPointer();
                planes[0] = vram + PlaneSize * 0;
                planes[1] = vram + PlaneSize * 1;
                planes[2] = vram + PlaneSize * 2;
                planes[3] = vram + PlaneSize * 3;
            }

            this.graphics = video.Graphics;
            this.sequencer = video.Sequencer;
        }
        #endregion

        #region Internal Methods
        /// <summary>
        /// Reads a byte from an address in video memory.
        /// </summary>
        /// <param name="offset">Address of byte to read.</param>
        /// <returns>Byte at specified address.</returns>
        internal override byte GetVramByte(uint offset)
        {
            offset %= 65536u;

            unsafe
            {
                latches[0] = planes[0][offset];
                latches[1] = planes[1][offset];
                latches[2] = planes[2][offset];
                latches[3] = planes[3][offset];
            }

            if((graphics.GraphicsMode & (1 << 3)) == 0)
                return latches[graphics.ReadMapSelect & 0x3];
            else
            {
                int result1 = graphics.ExpandedColorDontCare[0] & ~(latches[0] ^ graphics.ExpandedColorCompare[0]);
                int result2 = graphics.ExpandedColorDontCare[1] & ~(latches[1] ^ graphics.ExpandedColorCompare[1]);
                int result3 = graphics.ExpandedColorDontCare[2] & ~(latches[2] ^ graphics.ExpandedColorCompare[2]);
                int result4 = graphics.ExpandedColorDontCare[3] & ~(latches[3] ^ graphics.ExpandedColorCompare[3]);
                return (byte)(result1 | result2 | result3 | result4);
            }
        }
        /// <summary>
        /// Writes a byte to an address in video memory.
        /// </summary>
        /// <param name="offset">Address where byte will be written.</param>
        /// <param name="value">Value to write to specified address.</param>
        internal override void SetVramByte(uint offset, byte value)
        {
            offset %= 65536u;

            uint writeMode = graphics.GraphicsMode & 0x3u;
            if(writeMode == 0)
                SetByteMode0(offset, value);
            else if(writeMode == 1)
            {
                uint mapMask = sequencer.MapMask;
                unsafe
                {
                    if((sequencer.MapMask & 0x01) != 0)
                        planes[0][offset] = latches[0];
                    if((sequencer.MapMask & 0x02) != 0)
                        planes[1][offset] = latches[1];
                    if((sequencer.MapMask & 0x04) != 0)
                        planes[2][offset] = latches[2];
                    if((sequencer.MapMask & 0x08) != 0)
                        planes[3][offset] = latches[3];
                }
            }
            else
                SetByteMode2(offset, value);
        }
        /// <summary>
        /// Reads a 16-bit word from an address in video memory.
        /// </summary>
        /// <param name="offset">Address of word to read.</param>
        /// <returns>Word at specified address.</returns>
        internal override ushort GetVramWord(uint offset)
        {
            offset %= 65536u;

            uint latchOffset = offset + 1u;
            unsafe
            {
                latches[0] = planes[0][latchOffset];
                latches[1] = planes[1][latchOffset];
                latches[2] = planes[2][latchOffset];
                latches[3] = planes[3][latchOffset];
                return *(ushort*)(planes[graphics.ReadMapSelect & 0x3] + offset);
            }
        }
        /// <summary>
        /// Writes a 16-bit word to an address in video memory.
        /// </summary>
        /// <param name="offset">Address where word will be written.</param>
        /// <param name="value">Value to write to specified address.</param>
        internal override void SetVramWord(uint offset, ushort value)
        {
            SetVramByte(offset, (byte)value);
            SetVramByte(offset + 1u, (byte)(value >> 8));
        }
        /// <summary>
        /// Reads a 32-bit doubleword from an address in video memory.
        /// </summary>
        /// <param name="offset">Address of doubleword to read.</param>
        /// <returns>Doubleword at specified address.</returns>
        internal override uint GetVramDWord(uint offset)
        {
            offset %= 65536u;

            uint latchOffset = offset + 3u;
            unsafe
            {
                latches[0] = planes[0][latchOffset];
                latches[1] = planes[1][latchOffset];
                latches[2] = planes[2][latchOffset];
                latches[3] = planes[3][latchOffset];
                return *(uint*)(planes[graphics.ReadMapSelect & 0x3] + offset);
            }
        }
        /// <summary>
        /// Writes a 32-bit doubleword to an address in video memory.
        /// </summary>
        /// <param name="offset">Address where doubleword will be written.</param>
        /// <param name="value">Value to write to specified address.</param>
        internal override void SetVramDWord(uint offset, uint value)
        {
            SetVramByte(offset, (byte)value);
            SetVramByte(offset + 1u, (byte)(value >> 8));
            SetVramByte(offset + 2u, (byte)(value >> 16));
            SetVramByte(offset + 3u, (byte)(value >> 24));
        }
        /// <summary>
        /// Writes a character at a position on the screen with the current font.
        /// </summary>
        /// <param name="x">Column of character to write.</param>
        /// <param name="y">Row of character to write.</param>
        /// <param name="index">Index of character to write.</param>
        /// <param name="foreground">Foreground color of character to write.</param>
        /// <param name="background">Background color of character to write.</param>
        internal override void WriteCharacter(int x, int y, int index, byte foreground, byte background)
        {
            Graphics.ExpandRegister(foreground, expandedForeground);
            Graphics.ExpandRegister(background, expandedBackground);

            int stride = this.Stride;
            int startPos = y * stride * 16 + x;
            byte[] font = this.Font;
            
            for(int row = 0; row < 16; row++)
            {
                uint fgMask = font[index * 16 + row];
                uint bgMask = ~fgMask;
                uint value1 = (expandedForeground[0] & fgMask);
                uint value2 = (expandedForeground[1] & fgMask);
                uint value3 = (expandedForeground[2] & fgMask);
                uint value4 = (expandedForeground[3] & fgMask);

                unsafe
                {
                    if((background & 0x08) == 0)
                    {
                        planes[0][startPos + row * stride] = (byte)value1;
                        planes[1][startPos + row * stride] = (byte)value2;
                        planes[2][startPos + row * stride] = (byte)value3;
                        planes[3][startPos + row * stride] = (byte)value4;
                    }
                    else
                    {
                        planes[0][startPos + row * stride] ^= (byte)value1;
                        planes[1][startPos + row * stride] ^= (byte)value2;
                        planes[2][startPos + row * stride] ^= (byte)value3;
                        planes[3][startPos + row * stride] ^= (byte)value4;
                    }
                }
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Writes a byte to video RAM using the rules for write mode 0.
        /// </summary>
        /// <param name="offset">Video RAM offset to write byte.</param>
        /// <param name="input">Byte to write to video RAM.</param>
        private void SetByteMode0(uint offset, byte input)
        {
            if(graphics.DataRotate == 0)
            {
                byte mask = graphics.BitMask;

                for(int p = 0; p < 4; p++)
                {
                    if(sequencer.ExpandedMapMask[p])
                    {
                        byte source = input;

                        if(graphics.ExpandedEnableSetReset[p])
                            source = graphics.ExpandedSetReset[p];

                        source &= mask;
                        source |= (byte)(latches[p] & ~mask);

                        unsafe
                        {
                            planes[p][offset] = source;
                        }
                    }
                }
            }
            else
                SetByteMode0_Extended(offset, input);
        }
        /// <summary>
        /// Writes a byte to video RAM using the rules for write mode 0.
        /// </summary>
        /// <param name="offset">Video RAM offset to write byte.</param>
        /// <param name="input">Byte to write to video RAM.</param>
        /// <remarks>
        /// This method handles the uncommon case when DataRotate is not 0.
        /// </remarks>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void SetByteMode0_Extended(uint offset, byte input)
        {
            byte source;
            int rotateCount = graphics.DataRotate & 0x07;
            int logicalOp = (graphics.DataRotate >> 3) & 0x03;

            for(int p = 0; p < 4; p++)
            {
                int planeBit = 1 << p;
                if((sequencer.MapMask & planeBit) != 0)
                {
                    if((graphics.EnableSetReset & planeBit) != 0)
                        source = ((graphics.SetReset & planeBit) != 0) ? (byte)0xFF : (byte)0;
                    else
                        source = input;

                    uint a = (uint)(source >> rotateCount);
                    uint b = (uint)(source << (8 - rotateCount));
                    source = (byte)(a | b);
                    switch(logicalOp)
                    {
                    case 1:
                        source &= (byte)latches[p];
                        break;

                    case 2:
                        source |= (byte)latches[p];
                        break;

                    case 3:
                        source ^= (byte)latches[p];
                        break;
                    }

                    byte mask = graphics.BitMask;
                    source &= mask;
                    source |= (byte)(latches[p] & ~mask);

                    unsafe
                    {
                        planes[p][offset] = source;
                    }
                }
            }
        }
        /// <summary>
        /// Writes a byte to video RAM using the rules for write mode 2.
        /// </summary>
        /// <param name="offset">Video RAM offset to write byte.</param>
        /// <param name="input">Byte to write to video RAM.</param>
        private void SetByteMode2(uint offset, byte input)
        {
            byte value1 = 0;
            byte value2 = 0;
            byte value3 = 0;
            byte value4 = 0;

            if((input & 0x1) != 0)
                value1 = 0xFF;
            if((input & 0x2) != 0)
                value2 = 0xFF;
            if((input & 0x4) != 0)
                value3 = 0xFF;
            if((input & 0x8) != 0)
                value4 = 0xFF;

            int logicalOp = (graphics.DataRotate >> 3) & 0x03;
            switch(logicalOp)
            {
            case 1:
                value1 &= (byte)latches[0];
                value2 &= (byte)latches[1];
                value3 &= (byte)latches[2];
                value4 &= (byte)latches[3];
                break;

            case 2:
                value1 |= (byte)latches[0];
                value2 |= (byte)latches[1];
                value3 |= (byte)latches[2];
                value4 |= (byte)latches[3];
                break;

            case 3:
                value1 ^= (byte)latches[0];
                value2 ^= (byte)latches[1];
                value3 ^= (byte)latches[2];
                value4 ^= (byte)latches[3];
                break;
            }

            byte mask = graphics.BitMask;
            value1 &= mask;
            value2 &= mask;
            value3 &= mask;
            value4 &= mask;

            value1 |= (byte)(latches[0] & ~mask);
            value2 |= (byte)(latches[1] & ~mask);
            value3 |= (byte)(latches[2] & ~mask);
            value4 |= (byte)(latches[3] & ~mask);

            unsafe
            {
                if((sequencer.MapMask & 0x01) != 0)
                    planes[0][offset] = value1;
                if((sequencer.MapMask & 0x02) != 0)
                    planes[1][offset] = value2;
                if((sequencer.MapMask & 0x04) != 0)
                    planes[2][offset] = value3;
                if((sequencer.MapMask & 0x08) != 0)
                    planes[3][offset] = value4;
            }
        }
        #endregion

        #region Private Fields
        private readonly unsafe byte*[] planes = new byte*[4];
        private readonly byte[] latches = new byte[4];
        private readonly Graphics graphics;
        private readonly Sequencer sequencer;
        private readonly byte[] expandedForeground = new byte[4];
        private readonly byte[] expandedBackground = new byte[4];
        #endregion
    }
}
